/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package TimeRecording.TimeClockServer;

import TimeRecording.TimeClockServer.messages.ClockMessage;
import TimeRecording.TimeClockServer.database.ClockDatabase;
import TimeRecording.TimeClockServer.dtos.DatabaseTransaction;
import TimeRecording.TimeClockServer.enums.Component;
import TimeRecording.TimeClockServer.enums.ComponentStatus;
import TimeRecording.TimeClockServer.messages.rx.RxClockMessage;
import TimeRecording.TimeClockServer.messages.rx.TimeMessage;
import TimeRecording.TimeClockServer.messages.rx.UserTransactionMessage.UserTransaction;
import TimeRecording.TimeClockServer.messages.tx.PositiveResponseMessage;
import TimeRecording.TimeClockServer.messages.rx.UserTransactionMessage;
import TimeRecording.TimeClockServer.messages.tx.NegativeResponseMessage;
import java.io.IOException;
import java.sql.SQLException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * This class is at the heart of the clock server application.  It coordinates
 * communication between the clock terminal, the computer and the database.
 *
 * @author david
 */
public class ClockLogic extends Thread {

    // Frequency with which the clock time is polled (in milliseconds)
    private final static int TIME_REQUEST_PERIOD = 1 * 30;
    private final static int TIME_UPDATE_CHECK_PERIOD = 10;
    private final static int DOWNLOAD_CHECK_PERIOD = 10;
    private final static int HISTORY_UPLOAD_CHECK_PERIOD = 10;

    // Housekeeping will be queued at 22:00
    private final static int HOUSEKEEPING_QUEUE_HOUR = 22;

    // Housekeeping will fire at 23:00
    private final static int HOUSEKEEPING_FIRE_HOUR = 23;

    private GregorianCalendar housekeepingCalendar = new GregorianCalendar();
    boolean housekeepingRequired = true;

    private ClockServer parent;
    private RxBuffer rx = null;
    private TxBuffer tx = null;
    private ClockDatabase database;

    public ClockLogic(ClockServer parent) {
        this.parent = parent;
        this.rx = parent.getRxBuffer();
        this.tx = parent.getTxBuffer();
        database = new ClockDatabase(null, null, null, null);
    }

    @Override
    @SuppressWarnings("SleepWhileHoldingLock")
    public void run() {

        int timeRequestTimeout = TIME_REQUEST_PERIOD;
        int timeUpdateCheckTimeout = TIME_UPDATE_CHECK_PERIOD;
        int downloadCheckTimeout = DOWNLOAD_CHECK_PERIOD;
        int historyUploadCheckTimeout = HISTORY_UPLOAD_CHECK_PERIOD;
        
        try {
            // Fire off a time request as our first action
            tx.addMessage(ClockMessage.REQUEST_TIME);

            // Download data to the clock to get it talking
            downloadDataFile(database);

            while (!interrupted()) {

                // Anything ready to read?
                if (rx.messageReady()) {

                    RxClockMessage msg = new RxClockMessage(
                            rx.getMessage().getBytes());

                    // Check the database is online
                    parent.deferredStatusUpdate(Component.DATABASE,
                            (database.getDatabaseConnection() ?
                            ComponentStatus.ONLINE :
                            ComponentStatus.OFFLINE));

                    if (msg.isAck()) {
                        rx.recordAckReceived();

                    } else if (msg.isMsg("TR00")) {
                        processTimeMessage(msg);

                    } else if (msg.isMsg("PN00")) {
                        processTransactionMessage(msg);

                    // If we don't know what it is then don't ACK it
                    } else if (msg.ackRequired()) {
                        tx.recordAckRequired();
                    }

                } else {
                    Thread.sleep(1000);
                }

                if (timeRequestTimeout-- <= 0) {
                    tx.addMessage(ClockMessage.REQUEST_TIME);
                    timeRequestTimeout = TIME_REQUEST_PERIOD;
                }

                if (timeUpdateCheckTimeout-- <= 0) {
                    checkForTimeUpdateTime();
                    timeUpdateCheckTimeout = TIME_UPDATE_CHECK_PERIOD;
                }

                if (downloadCheckTimeout-- <= 0) {
                    checkForDownload();
                    downloadCheckTimeout = DOWNLOAD_CHECK_PERIOD;
                }
                
                if (historyUploadCheckTimeout-- <= 0) {
                    checkForHistoryUpload();
                    historyUploadCheckTimeout = HISTORY_UPLOAD_CHECK_PERIOD;
                }

                checkForHousekeeping();
            }
            
        } catch (InterruptedException ex) {
            Logger.getLogger(ClockLogic.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    private void checkForHousekeeping() {

        housekeepingCalendar.setTime(new Date());
        int hour = housekeepingCalendar.get(GregorianCalendar.HOUR_OF_DAY);

        if (hour == HOUSEKEEPING_QUEUE_HOUR) {
            housekeepingRequired = true;

        } else if (hour == HOUSEKEEPING_FIRE_HOUR && housekeepingRequired) {
            
            try {
                database.performHousekeeping();
                tx.addMessage(ClockMessage.REQUEST_HISTORY);

            } catch (SQLException ex) {
                Logger.getLogger(ClockLogic.class.
                        getName()).log(Level.SEVERE, null, ex);
            }
            housekeepingRequired = false;
        }
    }

    private void checkForTimeUpdateTime() {

        try {
            if (database.getTimeUpdateRequired()) {
                tx.recordTimeToBeTransmitted();
                database.clearTimeUpdateRequired();
            }
            
        } catch (SQLException ex) {
            Logger.getLogger(ClockLogic.class.
                    getName()).log(Level.SEVERE, null, ex);
        }
    }

    private void checkForDownload() {

        try {
            if (database.getDownloadRequired()) {
                downloadDataFile(database);
                database.clearDownloadRequired();
            }

        } catch (SQLException ex) {
            Logger.getLogger(ClockLogic.class.
                    getName()).log(Level.SEVERE, null, ex);
        }
    }

    private void checkForHistoryUpload() {

        try {
            if (database.getHistoryUploadRequired()) {
                tx.addMessage(ClockMessage.REQUEST_HISTORY);
                database.clearHistoryUploadRequired();
            }

        } catch (SQLException ex) {
            Logger.getLogger(ClockLogic.class.
                    getName()).log(Level.SEVERE, null, ex);
        }
    }

    private void downloadDataFile(ClockDatabase database) {
        
        // First thing we must do is download data
        try {
            ArrayList<ClockMessage> messages =
                    DownloadFile.buildDownloadFile(database);
            for (ClockMessage message : messages) {
                tx.addMessage(message);
            }

        } catch (IOException ex) {
            Logger.getLogger(ClockLogic.class.getName()).log(Level.SEVERE, null, ex);

        } catch (SQLException ex) {
            Logger.getLogger(ClockLogic.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    private void processTimeMessage(ClockMessage msg) {
        
        TimeMessage time = new TimeMessage(msg.getBytes());

        try {
            checkDrift(time.getClockTime());
            database.recordClockTime(time.getClockTime());
            
        } catch (ParseException ex) {
            Logger.getLogger(ClockLogic.class.getName()).log(Level.SEVERE, null, ex);

        } catch (SQLException ex) {
            Logger.getLogger(ClockLogic.class.getName()).log(Level.SEVERE, null, ex);
        }

        tx.recordAckRequired();
    }

    private boolean processTransactionMessage(ClockMessage msg) {

        try {
            UserTransactionMessage transaction =
                    new UserTransactionMessage(msg.getBytes());

            // If the transaction happened in real-time then test clock drift
            if (transaction.isInteractive()) {
                checkDrift(transaction.getMessageTime());
            }

            // Get a list of transactions from the message (as it happens, we'll
            // only be expecting to find a single one from prompt 0100A
            ArrayList<UserTransaction> userTransactions =
                    transaction.getTransactions();

            UserTransaction userTransaction = userTransactions.get(0);

            // Build a DTO for persistence to the database
            DatabaseTransaction dbTransaction = new DatabaseTransaction(
                    Integer.parseInt(userTransaction.getData()), null,
                    new Date(), transaction.getMessageTime(),
                    userTransaction.getPrompt(), null);

            database.clockInOrOut(dbTransaction);

            // Inform the GUI of the check in/out
            parent.deferredLogTransaction(dbTransaction);

            // Initiate acknowledgement of the message
            tx.recordAckRequired();

            // If there's no in/out then the badge is unknown to us
            if (dbTransaction.getClockedInOrOut() == null) {
                tx.addMessage(new NegativeResponseMessage(dbTransaction));
            } else if (dbTransaction.getBadgeDetails().getEmployeeId() == 0) {
                tx.addMessage(new NegativeResponseMessage(dbTransaction));
            } else {
                tx.addMessage(new PositiveResponseMessage(dbTransaction));
            }

            return true;

        } catch (SQLException ex) {
            Logger.getLogger(ClockLogic.class.getName()).log(Level.SEVERE, null, ex);
            return false;

        } catch (ParseException ex) {
            Logger.getLogger(ClockLogic.class.getName()).log(Level.SEVERE, null, ex);
            return false;
        }
    }

    private void checkDrift(Date clockDate) {
        if (clockDate != null) {
            Date systemDate = new Date();
            long drift = (clockDate.getTime() - systemDate.getTime()) / 1000;
            parent.deferredDriftUpdate(drift);
        }
    }
}
